﻿using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using JetBrains.Application.Progress;
using JetBrains.ProjectModel;
using JetBrains.ReSharper.Feature.Services.CSharp.Bulbs;
using JetBrains.ReSharper.Intentions.Util;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CodeAnnotations;
using JetBrains.ReSharper.Psi.CSharp;
using JetBrains.ReSharper.Psi.CSharp.Impl;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.CSharp.Util;
using JetBrains.ReSharper.Psi.Modules;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.ReSharper.Psi.Util;
using JetBrains.TextControl;
using JetBrains.Util;
using JetBrains.Util.Special;

namespace MugenMvvmToolkit.ReSharper.Infrastructure
{
    public abstract class ArgumentContextAction : ContextActionBaseExt
    {
        #region Constructors

        protected ArgumentContextAction([NotNull] ICSharpContextActionDataProvider provider)
            : base(provider)
        {
        }

        #endregion

        #region Overrides of ContextActionBase

        /// <summary>
        ///     Executes QuickFix or ContextAction. Returns post-execute method.
        /// </summary>
        /// <returns>
        ///     Action to execute after document and PSI transaction finish. Use to open TextControls, navigate caret, etc.
        /// </returns>
        protected override sealed Action<ITextControl> ExecutePsiTransaction(ISolution solution,
            IProgressIndicator progress)
        {
            ExecuteInternal();
            return null;
        }

        /// <summary>
        ///     Check if this action is available at the constructed context.
        ///     Actions could store precalculated info in <paramref name="cache" /> to share it between different actions
        /// </summary>
        /// <returns>
        ///     true if this bulb action is available, false otherwise.
        /// </returns>
        public override sealed bool IsAvailable(IUserDataHolder cache)
        {
            if (Method == null || !Method.IsValid())
                return false;
            ICSharpParameterDeclaration parameterDeclaration = FindParameterDeclaration();
            if (parameterDeclaration != null)
                return IsAvailableWithDeclaration(parameterDeclaration);
            IAccessorDeclaration setterDeclaration = FindSetterDeclaration();
            if (setterDeclaration != null)
                return IsAvailableWithSetterDeclaration(setterDeclaration);
            return false;
        }

        #endregion

        #region Methods

        protected static void RemoveExistingAttributes(IAttributesOwnerDeclaration attributesOwnerDeclaration,
            CodeAnnotationsCache cache)
        {
            var list = new List<IAttribute>();
            foreach (IAttribute attribute in attributesOwnerDeclaration.AttributesEnumerable)
            {
                IAttributeInstance attributeInstance = attribute.GetAttributeInstance();
                if (cache.IsAnnotationAttribute(attributeInstance, CodeAnnotationsCache.CanBeNullAttributeShortName) ||
                    cache.IsAnnotationAttribute(attributeInstance, CodeAnnotationsCache.NotNullAttributeShortName))
                    list.Add(attribute);
            }
            foreach (IAttribute attribute in list)
                attributesOwnerDeclaration.RemoveAttribute(attribute);
        }

        protected static bool IsParameterUsage(ICSharpExpression expression, IParameter parameter)
        {
            var referenceExpression = expression as IReferenceExpression;
            if (referenceExpression != null)
                return Equals(referenceExpression.Reference.Resolve().DeclaredElement, parameter);
            return false;
        }

        private void ExecuteInternal()
        {
            ICSharpParameterDeclaration parameterDeclaration = FindParameterDeclaration();
            if (parameterDeclaration != null)
            {
                ILambdaExpression declaration =
                    LambdaExpressionNavigator.GetByParameterDeclaration(
                        parameterDeclaration as ILambdaParameterDeclaration);
                if (declaration != null)
                {
                    if (declaration.BodyBlock == null)
                    {
                        ICSharpExpression csharpExpression = declaration.BodyExpression != null
                            ? declaration.BodyExpression.CopyWithResolve()
                            : null;
                        declaration.SetBodyBlock(Provider.ElementFactory.CreateEmptyBlock());
                        if (csharpExpression != null)
                            declaration.BodyBlock.AddStatementAfter(
                                Provider.ElementFactory.CreateStatement("return $0;", csharpExpression), null);
                    }
                    AddCheckToBlock(declaration.BodyBlock, parameterDeclaration,
                        parameterDeclaration.DeclaredElement,
                        new List<ILambdaParameterDeclaration>(declaration.ParameterDeclarations)
                            .ConvertAll(IdentityFunc<ILambdaParameterDeclaration, ICSharpDeclaration>.Converter));
                }
                IAnonymousMethodExpression parameterDeclaration3 =
                    AnonymousMethodExpressionNavigator.GetByParameterDeclaration(
                        parameterDeclaration as IAnonymousMethodParameterDeclaration);
                if (parameterDeclaration3 != null)
                {
                    if (parameterDeclaration3.Body == null)
                        parameterDeclaration3.SetBody(Provider.ElementFactory.CreateEmptyBlock());
                    AddCheckToBlock(parameterDeclaration3.Body, parameterDeclaration,
                        parameterDeclaration.DeclaredElement,
                        new List<IAnonymousMethodParameterDeclaration>(parameterDeclaration3.ParameterDeclarations)
                            .ConvertAll(
                                IdentityFunc<IAnonymousMethodParameterDeclaration, ICSharpDeclaration>.Converter));
                }
                var ownerDeclaration =
                    (ICSharpParametersOwnerDeclaration)
                        CSharpParametersOwnerDeclarationNavigator.GetByParameterDeclaration(
                            parameterDeclaration as IRegularParameterDeclaration);
                if (ownerDeclaration == null)
                    return;
                var method = ownerDeclaration as ICSharpFunctionDeclaration;
                if (method != null)
                {
                    if (method.Body == null)
                    {
                        if (!method.GetDocumentRange().IsValid())
                            return;
                        method.SetBody(Provider.ElementFactory.CreateEmptyBlock());
                    }
                    AddCheckToBlock(method.GetMethodBodyVisibleForUser(),
                        parameterDeclaration, parameterDeclaration.DeclaredElement,
                        new List<ICSharpParameterDeclaration>(ownerDeclaration.ParameterDeclarations).ConvertAll(
                            IdentityFunc<ICSharpParameterDeclaration, ICSharpDeclaration>.Converter));
                }
                var indexerDeclaration = ownerDeclaration as IIndexerDeclaration;
                if (indexerDeclaration == null)
                    return;
                foreach (IAccessorDeclaration accessorDeclaration in indexerDeclaration.AccessorDeclarationsEnumerable)
                {
                    IBlock body = accessorDeclaration.Body;
                    if (body != null && !IsArgumentChecked(body, parameterDeclaration.DeclaredElement))
                        AddCheckToBlock(accessorDeclaration.Body, parameterDeclaration,
                            parameterDeclaration.DeclaredElement,
                            new List<ICSharpParameterDeclaration>(indexerDeclaration.ParameterDeclarations)
                                .ConvertAll(IdentityFunc<ICSharpParameterDeclaration, ICSharpDeclaration>.Converter));
                }
            }
            else
            {
                IAccessorDeclaration setterDeclaration = FindSetterDeclaration();
                var accessor = (IAccessor)setterDeclaration.DeclaredElement;
                if (accessor == null)
                    return;
                IParameter parameter = accessor.Parameters.Last();
                if (setterDeclaration.Body == null)
                    setterDeclaration.SetBody(Provider.ElementFactory.CreateEmptyBlock());
                AddCheckToBlock(setterDeclaration.Body, null, parameter,
                    EmptyList<ICSharpDeclaration>.InstanceList);
                AddAnnotation(parameter, setterDeclaration.GetContainingTypeMemberDeclaration());
            }
        }

        private void AddCheckToBlock(IBlock block, IDeclaration parameterDeclaration, IDeclaredElement parameter,
            IEnumerable<ICSharpDeclaration> parameterDeclarations)
        {
            AddAnnotation(parameter, parameterDeclaration);
            AddCheckMethodToBlock(block, parameterDeclaration, parameter, parameterDeclarations);
        }

        private bool IsAvailableWithSetterDeclaration(IAccessorDeclaration declaration)
        {
            ICSharpIdentifier nameIdentifier = declaration.NameIdentifier;
            if (nameIdentifier == null || nameIdentifier.GetText() != "set" ||
                !nameIdentifier.GetTreeTextRange().Contains(Provider.TreeOffset))
                return false;
            IPropertyDeclaration accessorDeclaration = PropertyDeclarationNavigator.GetByAccessorDeclaration(declaration);
            if (accessorDeclaration != null && accessorDeclaration.IsAuto)
                return false;
            var accessor = declaration.DeclaredElement as IAccessor;
            if (accessor == null || accessor.IsAbstract ||
                (accessor.OwnerMember.IsAbstract || accessor.Parameters.Count == 0))
                return false;
            IParameter parameter = accessor.Parameters.Last();
            if (parameter.Kind == ParameterKind.OUTPUT)
                return false;
            if (!parameter.Type.IsReferenceType())
                return parameter.Type.IsNullable();
            return true;
        }

        private bool IsAvailableWithDeclaration(ICSharpParameterDeclaration parameterDeclaration)
        {
            if (!parameterDeclaration.IsValid())
                return false;
            ITypeUsage typeUsage = parameterDeclaration.TypeUsage;
            if (typeUsage == null || Provider.TreeOffset < typeUsage.GetTreeStartOffset())
                return false;
            IParameter declaredElement = parameterDeclaration.DeclaredElement;
            if (!IsExpectedArgumentType(declaredElement))
                return false;

            IAnonymousMethodExpression anonymousDeclaration = AnonymousMethodExpressionNavigator
                .GetByParameterDeclaration(parameterDeclaration as IAnonymousMethodParameterDeclaration);
            if (anonymousDeclaration != null)
            {
                return anonymousDeclaration.Body != null &&
                       !IsArgumentChecked(anonymousDeclaration.Body, declaredElement);
            }

            var ownerDeclaration = (ICSharpParametersOwnerDeclaration)CSharpParametersOwnerDeclarationNavigator
                .GetByParameterDeclaration(parameterDeclaration);
            if (ownerDeclaration == null)
                return false;
            var functionDeclaration = ownerDeclaration as ICSharpFunctionDeclaration;
            if (functionDeclaration != null)
            {
                return functionDeclaration.Body != null && !IsArgumentChecked(functionDeclaration.Body, declaredElement);
            }

            var indexerDeclaration = ownerDeclaration as IIndexerDeclaration;
            if (indexerDeclaration == null)
                return false;
            foreach (IAccessorDeclaration accessorDeclaration in indexerDeclaration.AccessorDeclarationsEnumerable)
            {
                if (accessorDeclaration.Body != null && !IsArgumentChecked(accessorDeclaration.Body, declaredElement))
                    return true;
            }
            return false;
        }

        private IAccessorDeclaration FindSetterDeclaration()
        {
            return Provider.GetSelectedElement<IAccessorDeclaration>(true, true);
        }

        private ICSharpParameterDeclaration FindParameterDeclaration()
        {
            return Provider.GetSelectedElement<ICSharpParameterDeclaration>(true, true);
        }

        #endregion

        #region Virtual and abstract members

        protected abstract IMethod Method { get; }

        protected virtual IDeclaredType Type
        {
            get { return TypeProvider.Should; }
        }

        /// <summary>
        ///     Gets the argument check statement.
        /// </summary>
        /// <returns>
        ///     The argument check statement
        /// </returns>
        protected virtual ICSharpStatement CreateArgumentCheckStatement(IDeclaredElement parameter)
        {
            string format = string.Format("$0.{0}($1, \"{1}\");", Method.ShortName, parameter.ShortName);
            return Provider.ElementFactory.CreateStatement(format, Type.GetTypeElement(), parameter);
        }

        protected virtual void AddAnnotation(IDeclaredElement parameter, IDeclaration toAnnotate)
        {
            IType type = parameter.Type();
            if (toAnnotate == null || (!type.IsReferenceType() && !type.IsPointerType() && !type.IsNullable()))
                return;
            CodeAnnotationsCache annotationsCache = toAnnotate
                .GetPsiServices()
                .GetCodeAnnotationsCache();
            var attributeTypeForElement = annotationsCache
                .GetNullableAttributeTypeForElement(toAnnotate, CodeAnnotationNullableValue.NOT_NULL) as IClass;

            if (attributeTypeForElement == null || !(toAnnotate is IAttributesOwnerDeclaration))
                return;
            if (attributeTypeForElement.GetAccessRights() == AccessRights.INTERNAL && !attributeTypeForElement.Module.AreInternalsVisibleTo(TypeProvider.PsiModule))
                return;

            var attributesOwnerDeclaration = (IAttributesOwnerDeclaration)toAnnotate;
            RemoveExistingAttributes(attributesOwnerDeclaration, annotationsCache);
            IAttribute attribute = CSharpElementFactory
                .GetInstance(toAnnotate, true)
                .CreateAttribute(attributeTypeForElement);
            attribute = attributesOwnerDeclaration.AddAttributeAfter(attribute, null);
            ContextActionUtils
                .FormatWithDefaultProfile(
                    (ITreeNode)(AttributeSectionNavigator.GetByAttribute(attribute) ?? (object)attribute));
        }

        protected virtual void AddCheckMethodToBlock(IBlock block, IDeclaration parameterDeclaration,
            IDeclaredElement parameter,
            IEnumerable<ICSharpDeclaration> parameterDeclarations)
        {
            AddAnnotation(parameter, parameterDeclaration);
            var statement = block.AddStatementAfter(CreateArgumentCheckStatement(parameter), GetBestAnchor(block, parameterDeclaration, parameterDeclarations));
            ContextActionUtils.FormatWithDefaultProfile(statement);
        }

        /// <summary>
        ///     Gets is argument checked.
        /// </summary>
        /// <returns>
        ///     The get is argument checked.
        /// </returns>
        /// <remarks>
        ///     This method needs to be overridden
        /// </remarks>
        protected virtual bool IsArgumentChecked(ITreeNode block, IParameter parameter)
        {
            bool found = false;
            block.ProcessDescendants(new OneActionProcessor(element =>
            {
                var expression = element as IInvocationExpression;
                if (expression == null)
                    return;
                var referenceExpression = expression.InvokedExpression as IReferenceExpression;
                if (referenceExpression == null)
                    return;
                var resolveResult = referenceExpression.Reference.Resolve().DeclaredElement as IMethod;
                if (resolveResult != null && IsCheckMethod(resolveResult, parameter, expression.ArgumentList))
                    found = true;
            }, node => !found));
            return found;
        }

        protected virtual bool IsCheckMethod(IMethod method, IParameter parameter, IArgumentList argumentList)
        {
            return Method.Equals(method) && IsParameterUsage(argumentList.Arguments[0].Value, parameter);
        }

        /// <summary>
        ///     Gets a value indicating whether IsArgumentTypeCompatible.
        /// </summary>
        /// <returns>
        ///     <c>true</c> if the argument type is of the expected type, otherwise <c>false</c>.
        /// </returns>
        protected virtual bool IsExpectedArgumentType(IParameter declaredElement)
        {
            return declaredElement != null && declaredElement.Kind != ParameterKind.OUTPUT &&
                   (declaredElement.Type.IsReferenceType() || declaredElement.Type.IsPointerType() ||
                    declaredElement.Type.IsNullable()) && declaredElement.ShortName != "???";
        }

        protected virtual ICSharpStatement GetBestAnchor(IBlock block, IDeclaration parameterDeclaration, IEnumerable<ICSharpDeclaration> parameterDeclarations)
        {
            var dictionary = new Dictionary<ICSharpDeclaration, ICSharpStatement>();
            foreach (var statement in block.Statements.OfType<IExpressionStatement>())
            {
                var invokeExpression = statement.Expression as IInvocationExpression;
                if (invokeExpression == null || invokeExpression.ExtensionQualifier == null || invokeExpression.ArgumentList == null || invokeExpression.ArgumentList.Arguments.Count != 2)
                    continue;
                var referenceExpression = (invokeExpression.InvokedExpression as IReferenceExpression)
                    .IfNotNull(expression => expression.QualifierExpression) as IReferenceExpression;
                if (referenceExpression == null)
                    continue;
                var @class = referenceExpression.Reference.Resolve().DeclaredElement as IClass;
                if (@class == null || !@class.IsOfType(Type))
                    continue;

                var arg1 = invokeExpression.ArgumentList.Arguments[0];
                var arg2 = invokeExpression.ArgumentList.Arguments[1];
                referenceExpression = arg1.Expression as IReferenceExpression;
                if (referenceExpression == null)
                    continue;
                var parameter = referenceExpression.Reference.Resolve().DeclaredElement as IParameter;
                if (parameter == null || !Equals(parameter.ShortName, arg2.Expression.IfNotNull(expression => expression.ConstantValue.Value)))
                    continue;
                foreach (var index in parameterDeclarations)
                {
                    if (Equals(index.DeclaredElement, parameter))
                    {
                        dictionary[index] = statement;
                        break;
                    }
                }
            }
            ICSharpStatement anchor = null;
            foreach (var declaration in parameterDeclarations)
            {
                if (declaration == parameterDeclaration)
                    break;
                if (dictionary.ContainsKey(declaration))
                    anchor = dictionary[declaration];
            }
            return anchor;
        }

        #endregion
    }
}